// Triangle.cpp

#include <iostream>
#include <GLTools.h>            // OpenGL toolkit
#include <GLFrame.h>
#include <GLFrustum.h>
#include <GLMatrixStack.h>
#include <GLGeometryTransform.h>
#include <StopWatch.h>
#include <vector>
#include "utils.h"

#ifdef __APPLE__
#include <glut/glut.h>          // OS X version of GLUT
#else
#define FREEGLUT_STATIC
#include <GL/glut.h>            // Windows FreeGlut equivalent
#endif

#define M_PI       3.14159265358979323846

GLuint shader;
GLint ambientLightLocation;
GLint MVMatrixLocation;
GLint MVPMatrixLocation;
GLint normalMatrixLocation;
GLint shaderPositionLocation;
GLint shaderColorLocation;
GLint shaderAngleLocation;
GLint shaderAttenuation0Location;
GLint shaderAttenuation1Location;
GLint shaderAttenuation2Location;
GLint materialAmbientColorLocation;
GLint materialDiffuseColorLocation;
GLint materialSpecularColorLocation;
GLint materialSpecularExponentLocation;
GLint doTextureLocation;
GLint textureLocation;

GLFrame cameraFrame;
GLFrustum viewFrustum;
M3DMatrix44f cameraMatrix;	
int faces_nr;
GLuint textureID[1];

GLfloat ambient_light[] = {0.5f, 0.5f, 0.5f};

struct point_light point_light1 = 
{
	{2.0f, 2.0f, 2.5f},
	{1.0f, 1.0f, 1.0f},
	0.1f,
	0.1f,
	0.1f,
	0,
};

struct material_properties floor_mat_prop = 
{
	{0.2f, 0.7f, 0.2f},
	{0.7f, 0.3f, 0.1f},
	{0.2f, 0.2f, 0.2f},
	200,
};

struct material_properties grid_mat_prop =
{
	{0.0f},
	{0.0f},
	{0.0f},
	0.0f,
};

///////////////////////////////////////////////////////////////////////////////
// Window has changed size, or has just been created. In either case, we need
// to use the window dimensions to set the viewport and the projection matrix.

void ChangeSize(int w, int h) 
{    
	glViewport(0, 0, w, h);
	viewFrustum.SetPerspective(50.0f, float(w) / float(h), 
                               1.0, 200.0);
}

///////////////////////////////////////////////////////////////////////////////
// This function does any needed initialization on the rendering context.
// This is the first opportunity to do any OpenGL related tasks.

void SetupRC() 
{
	int vertex_nr;
	GLuint vertex_buffer;
	GLuint faces_buffer;
	std::vector<float> vertices;
	std::vector<GLint> faces;

	GLbyte *pBits;
	int nWidth, nHeight, nComponents;
	GLenum eFormat;

	M3DVector3f forward = {-5.0f, -5.0f, -5.0f};
    M3DVector3f origin = {5.0f, 5.0f, 5.0f};
    M3DVector3f up = {0.0f, 0.0f, 1.0f};
 
    SetUpFrame(cameraFrame, origin, forward, up);

	/* setting camera */
	cameraFrame.GetCameraMatrix(cameraMatrix);

    /* white background */
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
	
	/* shader location retrieving */
    shader = gltLoadShaderPairWithAttributes("pass_thru_shader.vp", "pass_thru_shader.fp",
											 3, 
											 GLT_ATTRIBUTE_VERTEX, "vVertex", 
											 GLT_ATTRIBUTE_NORMAL, "vNormal",
											 GLT_ATTRIBUTE_TEXTURE0, "texCoord0");
	
	GetUniformLocationExt(&MVMatrixLocation, "MVMatrix");
	GetUniformLocationExt(&MVPMatrixLocation, "MVPMatrix");
	GetUniformLocationExt(&normalMatrixLocation, "normalMatrix");
	GetUniformLocationExt(&shaderAngleLocation, "light1.angle");
	GetUniformLocationExt(&shaderPositionLocation, "light1.position");
	GetUniformLocationExt(&shaderColorLocation, "light1.color");
	GetUniformLocationExt(&shaderAttenuation0Location, "light1.attenuation0");
	GetUniformLocationExt(&shaderAttenuation1Location, "light1.attenuation1");
	GetUniformLocationExt(&shaderAttenuation2Location, "light1.attenuation2");
	GetUniformLocationExt(&ambientLightLocation, "ambientLight");
	GetUniformLocationExt(&materialAmbientColorLocation, "material.ambient_color");
	GetUniformLocationExt(&materialDiffuseColorLocation, "material.diffuse_color");
	GetUniformLocationExt(&materialSpecularColorLocation, "material.specular_color");
	GetUniformLocationExt(&materialSpecularExponentLocation, "material.specular_exponent");
	GetUniformLocationExt(&doTextureLocation, "doTexture");
	GetUniformLocationExt(&textureLocation, "texture0");

	/* loading vertices from file */
	vertex_nr = ReadVertices(vertices);
	faces_nr = ReadWalls(faces);

	glGenBuffers(1,&vertex_buffer);
	glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
	glBufferData(GL_ARRAY_BUFFER, vertex_nr*sizeof(float)*7, &vertices[0], GL_STATIC_DRAW);
	if	(glGetError() != GL_NO_ERROR) 
	{
		std::cerr << "error copying vertices" << std::endl;
	}
	glVertexAttribPointer(GLT_ATTRIBUTE_VERTEX, 4, GL_FLOAT, GL_FALSE, sizeof(float)*7, (const GLvoid *)0);
	glVertexAttribPointer(GLT_ATTRIBUTE_NORMAL, 3, GL_FLOAT, GL_FALSE, sizeof(float)*7, (const GLvoid *)(4*sizeof(float)));
	glEnableVertexAttribArray(GLT_ATTRIBUTE_VERTEX);
	glEnableVertexAttribArray(GLT_ATTRIBUTE_NORMAL);	

	glGenBuffers(1, &faces_buffer);
	if (glGetError() != GL_NO_ERROR)
	{
		std::cerr << "faces_buffer invalid" << std::endl;
	}
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, faces_buffer);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, faces_nr*sizeof(GLuint)*3, &faces[0], GL_STATIC_DRAW);
	if (glGetError() != GL_NO_ERROR) 
	{
		std::cerr << "error copying faces" << std::endl;
	}

	/* texture unit loading */
	glActiveTexture(GL_TEXTURE0);
	glGenTextures(1, textureID);
	glBindTexture(GL_TEXTURE_2D, textureID[0]);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

	pBits = gltReadTGABits("ground_1024_v1.tga", &nWidth, &nHeight, &nComponents, &eFormat);
	glTexImage2D(GL_TEXTURE_2D, 0, nComponents, nWidth, nHeight, 0, eFormat, GL_UNSIGNED_BYTE, pBits);
	glUseProgram(shader);
}

///////////////////////////////////////////////////////////////////////////////
// Called to draw scene

void RenderScene(void) 
{	
	CStopWatch timer; 
	GLMatrixStack modelView;
	GLMatrixStack projection;
	GLGeometryTransform transformPipeline;
	GLfloat light_transformed[3];

	M3DVector3f eye;
	M3DVector3f at = {0, 0, 0};
	M3DVector3f up = {0, 0, 1};
	static float _angle = 0;

	_angle += 5 * timer.GetElapsedSeconds() * M_PI;
	if (_angle > 2 * M_PI)
	{
		_angle = 0;
	}
	eye[0] = 6.8f * cos(_angle);
	eye[1] = 6.8f * sin(_angle);
	eye[2] = 5.0f; 
	LookAt(cameraFrame, eye, at, up);
	
	transformPipeline.SetMatrixStacks(modelView, projection);

	projection.LoadMatrix(viewFrustum.GetProjectionMatrix());
	cameraFrame.GetCameraMatrix(cameraMatrix);
	modelView.LoadMatrix(cameraMatrix);
	
    /* Clear the window with current clearing color */
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
	glEnable(GL_CULL_FACE);
	glCullFace(GL_BACK );
	glBindTexture(GL_TEXTURE_2D, textureID[0]);

	m3dTransformVector3(light_transformed, point_light1.position, cameraMatrix);

	glUniform3fv(ambientLightLocation, 1, ambient_light);
	glUniform3fv(materialAmbientColorLocation, 1, floor_mat_prop.ambient_color);
	glUniform3fv(materialDiffuseColorLocation, 1, floor_mat_prop.diffuse_color);
	glUniform3fv(materialSpecularColorLocation, 1, floor_mat_prop.specular_color);
	glUniform1f(materialSpecularExponentLocation, floor_mat_prop.specular_exponent);
	glUniform3fv(shaderPositionLocation, 1, light_transformed);
	glUniform3fv(shaderColorLocation, 1, point_light1.color);
	glUniform1f(shaderAngleLocation, point_light1.angle);
	glUniform1f(shaderAttenuation0Location, point_light1.attenuation0);
	glUniform1f(shaderAttenuation1Location, point_light1.attenuation1);
	glUniform1f(shaderAttenuation2Location, point_light1.attenuation2);

	/* drawGrid */
	glUniformMatrix4fv(MVPMatrixLocation, 1, GL_FALSE, transformPipeline.GetModelViewProjectionMatrix());
	glUniformMatrix4fv(MVMatrixLocation, 1, GL_FALSE, transformPipeline.GetModelViewMatrix());
	glUniformMatrix3fv(normalMatrixLocation, 1, GL_FALSE, transformPipeline.GetNormalMatrix());
	glUniform1i(textureLocation, 0);
	glUniform1i(doTextureLocation, 1);

	glPolygonOffset(1.0f, 1.0f);
	glEnable(GL_POLYGON_OFFSET_FILL);
	
	DrawFloor(-10, -10, 10, 10);
	glDisable(GL_POLYGON_OFFSET_FILL);

	glUniform3fv(materialAmbientColorLocation, 1, grid_mat_prop.ambient_color);
	DrawGrid(-10, -10, 10, 10, 20, 0);
	glUniform3fv(materialAmbientColorLocation, 1, floor_mat_prop.ambient_color);

	glUniform1i(doTextureLocation, 0);
	glDrawElements(GL_TRIANGLES, 3*faces_nr, GL_UNSIGNED_INT, 0);

    // Perform the buffer swap to display back buffer
    glutSwapBuffers();
	glutPostRedisplay();
}


///////////////////////////////////////////////////////////////////////////////
// Main entry point for GLUT based programs

int main(int argc, char* argv[]) 
{  
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
    glutInitWindowSize(800, 600);
    glutCreateWindow("Triangle");
    glutReshapeFunc(ChangeSize);
    glutDisplayFunc(RenderScene);

    GLenum err = glewInit();
    if (GLEW_OK != err) {
        std::cerr << "GLEW Error: " << glewGetErrorString(err) << std::endl;
        return 1;
    }

    SetupRC();

    glutMainLoop();
    return 0;
}
